web/src/routes/image/[id]/+page.svelte (view raw)
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { getImage, imageDownloadUrl, thumbnailUrl, type ImageDetail } from '$lib/api';
4
5 let { params } = $props();
6 let id = $derived(parseInt(params.id));
7
8 let img = $state<ImageDetail | null>(null);
9 let loading = $state(true);
10 let error = $state('');
11
12 onMount(async () => {
13 try {
14 img = await getImage(id);
15 } catch (err) {
16 error = err instanceof Error ? err.message : 'Failed to load image';
17 } finally {
18 loading = false;
19 }
20 });
21
22 function formatSize(bytes: number): string {
23 if (bytes < 1024) return bytes + ' B';
24 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
25 return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
26 }
27
28 function formatDate(s: string): string {
29 return new Date(s).toLocaleString();
30 }
31</script>
32
33{#if loading}
34 <p class="state-msg">Loading...</p>
35{:else if error}
36 <p class="state-msg error">{error}</p>
37{:else if img}
38 <div class="page-head">
39 <h1>{img.filename}</h1>
40 <div class="head-line"></div>
41 </div>
42
43 <div class="layout">
44 <div class="preview">
45 {#if img.thumbnail_path}
46 <img src={imageDownloadUrl(img.id)} alt={img.filename} />
47 {:else}
48 <div class="no-thumb">NO THUMBNAIL</div>
49 {/if}
50 </div>
51
52 <div class="meta">
53 <div class="meta-block">
54 <span class="meta-label">DIMENSIONS</span>
55 <span class="meta-value">{img.width} × {img.height}</span>
56 </div>
57 <div class="meta-block">
58 <span class="meta-label">FILE SIZE</span>
59 <span class="meta-value">{formatSize(img.file_size)}</span>
60 </div>
61 <div class="meta-block">
62 <span class="meta-label">TYPE</span>
63 <span class="meta-value">{img.mime_type}</span>
64 </div>
65 <div class="meta-block">
66 <span class="meta-label">UPLOADED</span>
67 <span class="meta-value">{formatDate(img.created_at)}</span>
68 </div>
69 <div class="meta-block tags-block">
70 <span class="meta-label">TAGS</span>
71 <div class="tag-pills">
72 {#if img.tags_detail.length > 0}
73 {#each img.tags_detail as t (t.tag)}
74 <a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}">
75 <span class="tag-name">{t.tag}</span>
76 <span class="tag-count">{t.usage_count}</span>
77 </a>
78 {/each}
79 {:else}
80 <span class="none">none</span>
81 {/if}
82 </div>
83 </div>
84
85 <a href={imageDownloadUrl(img.id)} class="btn" download={img.filename}>
86 DOWNLOAD ORIGINAL
87 </a>
88 </div>
89 </div>
90
91 <p class="back"><a href="/browse">← BACK TO BROWSE</a></p>
92{/if}
93
94<style>
95 .state-msg {
96 padding: 2rem 0;
97 font-size: 1.1rem;
98 color: #888;
99 }
100 .error {
101 color: #c62828;
102 }
103
104 .page-head {
105 margin-bottom: 1.5rem;
106 }
107 .page-head h1 {
108 word-break: break-all;
109 }
110 .head-line {
111 height: 3px;
112 background: #c62828;
113 width: 3rem;
114 margin-top: 0.4rem;
115 }
116
117 .layout {
118 display: flex;
119 gap: 2rem;
120 flex-wrap: wrap;
121 }
122 .preview {
123 flex: 1;
124 min-width: 300px;
125 background: #1a1a1a;
126 display: flex;
127 align-items: center;
128 justify-content: center;
129 }
130 .preview img {
131 display: block;
132 width: 100%;
133 height: auto;
134 max-height: 70vh;
135 object-fit: contain;
136 }
137 .no-thumb {
138 padding: 4rem 2rem;
139 color: #555;
140 font-family: 'Oswald', sans-serif;
141 font-weight: 500;
142 font-size: 0.9rem;
143 letter-spacing: 0.12em;
144 }
145
146 .meta {
147 width: 260px;
148 flex-shrink: 0;
149 display: flex;
150 flex-direction: column;
151 gap: 1rem;
152 }
153 .meta-block {
154 display: flex;
155 flex-direction: column;
156 gap: 0.15rem;
157 }
158 .meta-label {
159 font-family: 'Oswald', sans-serif;
160 font-weight: 500;
161 font-size: 0.7rem;
162 letter-spacing: 0.15em;
163 color: #999;
164 }
165 .meta-value {
166 font-family: 'Inter', sans-serif;
167 font-size: 0.9rem;
168 font-weight: 600;
169 color: #1a1a1a;
170 }
171
172 .tags-block {
173 flex: 1;
174 }
175 .tag-pills {
176 display: flex;
177 flex-wrap: wrap;
178 gap: 0.35rem;
179 }
180 .tag-pill {
181 display: inline-flex;
182 align-items: center;
183 gap: 0.3rem;
184 background: #1a1a1a;
185 padding: 0.25rem 0.5rem 0.25rem 0.65rem;
186 font-family: 'Inter', sans-serif;
187 font-size: 0.78rem;
188 font-weight: 500;
189 color: #eee;
190 text-decoration: none;
191 transition: background 0.15s;
192 }
193 .tag-pill:hover {
194 background: #c62828;
195 text-decoration: none;
196 }
197 .tag-name {
198 font-weight: 600;
199 }
200 .tag-count {
201 background: rgba(255, 255, 255, 0.12);
202 padding: 0.05rem 0.35rem;
203 font-size: 0.68rem;
204 font-weight: 600;
205 color: rgba(255, 255, 255, 0.65);
206 min-width: 1.1rem;
207 text-align: center;
208 }
209 .tag-pill:hover .tag-count {
210 background: rgba(0, 0, 0, 0.2);
211 }
212 .none {
213 color: #bbb;
214 font-size: 0.85rem;
215 }
216
217 .btn {
218 display: inline-flex;
219 align-items: center;
220 align-self: flex-start;
221 padding: 0.55rem 1.25rem;
222 background: #c62828;
223 color: #fff;
224 border: none;
225 font-family: 'Oswald', sans-serif;
226 font-weight: 500;
227 font-size: 0.8rem;
228 letter-spacing: 0.1em;
229 text-decoration: none;
230 text-transform: uppercase;
231 cursor: pointer;
232 transition: background 0.15s;
233 margin-top: 0.5rem;
234 }
235 .btn:hover {
236 background: #b71c1c;
237 text-decoration: none;
238 }
239
240 .back {
241 margin-top: 2rem;
242 }
243 .back a {
244 font-family: 'Oswald', sans-serif;
245 font-weight: 500;
246 font-size: 0.85rem;
247 letter-spacing: 0.1em;
248 }
249</style>